#include "meshImportExport.hpp"
#include "meshTypeFunctions.hpp"
#include "mesh.hpp"
#include "uniMesh.hpp"
#include "rectMesh.hpp"
#include "structMesh.hpp"
#include "mesh_testFunctions.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>

using namespace hms;
using Catch::Matchers::ContainsSubstring;

TEST_CASE( "Finding edges at boundary vertices" ){
	SECTION( "Rectangular mesh" ){
		auto mesh { testMeshRectangles(true) };

		auto result {
			edgesAtBoundaryVertices( mesh )
		};

		Array3Xi expectedResult (3, 8);
		expectedResult <<
			0, 1, 2, 3, 5, 6, 7, 8,
			6, 8, 9, 6,10, 4, 4, 5,
			8, 9,10, 7,11, 7, 5,11;

		REQUIRE( (result == expectedResult).all() );
	}
	SECTION( "Triangular mesh" ){
		auto mesh { testMeshTriangles(true) };

		auto result {
			edgesAtBoundaryVertices( mesh )
		};

		Array3Xi expectedResult (3, 6);
		expectedResult <<
			0, 1, 2, 3, 4, 5,
			3, 4, 5, 6, 7, 3,
			4, 5, 6, 7, 8, 8;

		REQUIRE( (result == expectedResult).all() );
	}
	SECTION( "Mixed mesh" ){
		auto mesh { testMeshMixed(true) };

		auto result {
			edgesAtBoundaryVertices( mesh )
		};

		Array3Xi expectedResult (3, 5);
		expectedResult <<
			0, 1, 2, 3, 4,
			1, 2, 1, 3, 4,
			2, 3, 4, 5, 5;
		
		REQUIRE( (result == expectedResult).all() );
	}
}

TEST_CASE( "Finding boundary segments between vertices" ){
	SECTION("Unstructured mesh"){
		auto mesh { testMeshRectangles(true) };

		auto edgesAtBoundaryVerts {
			edgesAtBoundaryVertices( mesh )
		};

		auto getSegment = [&](Index first, Index next, Index last){
			return verticesToBoundarySegment(
				mesh.edges().vertices(), edgesAtBoundaryVerts, first, next, last
			);
		};

		REQUIRE( (getSegment(7,8,2) == Array3i{5, 11, 10}).all() );
		REQUIRE( (getSegment(8,7,2) == ArrayNi<6>{5, 4, 7, 6, 8, 9}).all() );
		REQUIRE( (getSegment(7,8,8) == ArrayNi<1>{5}).all() );
		REQUIRE( (getSegment(7,8,7) == ArrayNi<8>{5, 11, 10, 9, 8, 6, 7, 4}).all() );
		REQUIRE( (getSegment(7,8,-1) == ArrayNi<8>{5, 11, 10, 9, 8, 6, 7, 4}).all() );
		REQUIRE_THROWS( getSegment(7,5,2) );
		REQUIRE_THROWS( getSegment(7,4,2) );
		REQUIRE_THROWS( getSegment(4,7,2) );
	}
	SECTION("Structured mesh"){
		auto mesh { uniMeshAs<UniMesh>(3, 4, 1, 1) };

		auto checkSegment = [&](Index first, Index next, Index last, const auto& arr){
			REQUIRE( (verticesToBoundarySegment(
				mesh, Array3i{ first, next, last }
			) == arr).all() );
		};

		/* ascending indexes within sides */
		checkSegment(1,2,2, ArrayNi<1>{1});
		checkSegment(1,2,3, Array2i{1,2});
		checkSegment(3,7,15, Array3i{18,22,26});
		checkSegment(4,8,16, Array3i{19,23,27});
		checkSegment(16,17,18, Array2i{12,13});
		/* descending indexes within sides */
		checkSegment(3,2,0, Array3i{2,1,0});
		checkSegment(15,11,3, Array3i{26,22,18});
		checkSegment(16,12,4, Array3i{27,23,19});
		checkSegment(18,17,16, Array2i{13,12});
		/* ascending across a corner */
		checkSegment(0,1,15, ArrayNi<6>{0,1,2,18,22,26});
		checkSegment(12,16,19, ArrayNi<4>{27,12,13,14});
		/* descending across a corner */
		checkSegment(19,15,0, ArrayNi<7>{30,26,22,18,2,1,0});
		checkSegment(18,17,8, Array4i{13,12,27,23});
		/* ascending and descending */
		checkSegment(11,15,17, ArrayNi<4>{26,30,14,13});
		checkSegment(3,7,16, ArrayNi<7>{18,22,26,30,14,13,12});
		checkSegment(17,18,11, ArrayNi<4>{13,14,30,26});
		checkSegment(16,17,3, ArrayNi<7>{12,13,14,30,26,22,18});
		/* across multiple corners */
		checkSegment(1,2,4, ArrayNi<12>{1,2,18,22,26,30,14,13,12,27,23,19});
		checkSegment(8,12,4, ArrayNi<13>{23,27,12,13,14,30,26,22,18,2,1,0,15});
		/* all around */
		checkSegment(19,15,19, ArrayNi<14>{30,26,22,18,2,1,0,15,19,23,27,12,13,14});
		checkSegment(2,3,2, ArrayNi<14>{2,18,22,26,30,14,13,12,27,23,19,15,0,1});

		/* verify input */
		auto mustThrow = [&](Index a, Index b, Index c, std::string&& message){
			REQUIRE_THROWS_WITH(
				verticesToBoundarySegment( mesh, Array3i{ a, b, c } ),
				ContainsSubstring( message.c_str() )
			);
		};
		/* out of bounds */
		mustThrow(-1,0,1, "out of bounds");
		mustThrow(17,18,20, "out of bounds");
		
		/* not at boundary */
		mustThrow(5,6,1, "not at boundary");
		mustThrow(2,6,0, "not at boundary");
		mustThrow(0,1,5, "not at boundary");

		/* wrong difference between first and second vertex */
		mustThrow(0,2,3, "difference");
	}
}

TEST_CASE( "Converting a boundary segment to defining vertices and back" ){
	SECTION( "Unstructured mesh" ){
		auto mesh { testMeshRectangles(true) };

		auto edgesAtBoundaryVerts {
			edgesAtBoundaryVertices( mesh )
		};

		ArrayXi boundarySegment (3);


		auto test = [&](Index first, Index second, Index last){
			Array3i verts { first, second, last };
			/* Conversion to vertices */
			REQUIRE(
				(boundarySegmentToVertices(mesh, boundarySegment) == verts).all()
			);
			/* conversion back to a boundary segment */
			REQUIRE(
				(verticesToBoundarySegment( mesh, verts, edgesAtBoundaryVerts)
					== boundarySegment).all()
			);
		};


		boundarySegment << 6, 7, 4;
		test(0, 3, 7);

		boundarySegment << 8, 9, 10;
		test(0, 1, 5);

		boundarySegment.resize(8);
		boundarySegment << 5, 11, 10, 9, 8, 6, 7, 4;
		test(7, 8, 7);

		/* segment with size 1 */
		boundarySegment.resize(1);
		boundarySegment << 5;
		test(7, 8, 8);
	}
	SECTION( "Structured mesh" ){
		auto mesh { uniMeshAs<UniMesh>(3, 4, 1, 1) };

		auto test = [&]( const ArrayXi& segment, Index a, Index b, Index c ){
			Array3i verts { a, b, c };
			/* conversion to vertices */
			REQUIRE(
				(boundarySegmentToVertices(mesh, segment) == verts).all()
			);
			/* conversion back to a boundary segment */
			REQUIRE(
				(verticesToBoundarySegment(mesh, verts) == segment).all()
			);
		};

		test( Array3i{19,23,27}, 4,8,16);
		test( Array3i{27,23,19}, 16,12,4);
		test( ArrayNi<7>{1,2,18,22,26,30,14}, 1,2,18 );
		test( ArrayNi<7>{1,2,18,22,26,30,14}.reverse(), 18,19, 1 );
		test( ArrayNi<8>{23,27,12,13,14,30,26,22}, 8,12,7 );
		test( ArrayNi<8>{23,27,12,13,14,30,26,22}.reverse(), 7,11,8 );
	}
}

TEST_CASE("Converting defining vertices to patches for struct. mesh"){
	auto mesh { UniMesh{3,4,1,1} };

	auto test = [&](Index a, Index b, Index c, std::vector<ArrayXi>&& target){
		std::vector<ArrayXi> patches {
			verticesToStructPatches( Array3i{a,b,c}, mesh.size() )
		};
		int i {0};
		for ( const auto& patch : patches ){
			REQUIRE( (patch == target[i++]).all() );
		}
	};

	Array3i bottom {0, 1, 2}, top {12, 13, 14};
	Array4i left {15, 19, 23, 27}, right {18, 22, 26, 30};

	SECTION("single side"){
		test(0,1,2, {bottom.head(2)});
		test(0,1,3, {bottom});
		test(1,2,3, {bottom.tail(2)});
		test(3,2,1, {bottom.tail(2)});
		test(3,2,0, {bottom});
		test(3,7,15, {right.head(3)});
		test(3,7,19, {right});
		test(7,11,15, {right.segment(1,2)});
		test(7,11,19, {right.tail(3)});
		test(19,15,7, {right.tail(3)});
		test(19, 15, 3, {right});
		test(15,11,7, {right.segment(1,2)});
		test(15,11,3, {right.head(3)});
		test(0,4,12, {left.head(3)});
		test(4,8,16, {left.tail(3)});
		test(16,12,4, {left.tail(3)});
		test(16,12,0, {left});
		test(16,17,18, {top.head(2)});
		test(16,17,19, {top});
		test(19,18,17, {top.tail(2)});
		test(19,18,16, {top});
	}
	SECTION("multiple sides"){
		/* start at bottom, with two to five patches, then reverse */
		test(0,1,7, {bottom, right.head(1)});
		test(1,2,19, {bottom.tail(2), right});
		test(1,2,17, {bottom.tail(2), right, top.tail(2)});
		test(0,1,16, {bottom, right, top});
		test(2,3,8, {bottom.tail(1), right, top, left.tail(2)});
		test(0,1,0, {bottom, right, top, left});
		test(1,2,1, {bottom.tail(2), right, top, left, bottom.head(1)});
		test(2,3,1, {bottom.tail(1), right, top, left, bottom.head(1)});
		test(3,2,3, {bottom, left, top, right});
		test(1,0,2, {bottom.head(1), left, top, right, bottom.tail(1)});
		test(2,1,3, {bottom.head(2), left, top, right});
		test(2,1,15, {bottom.head(2), left, top, right.tail(1)});
		test(3,2,19, {bottom, left, top});
		test(1,0,17, {bottom.head(1), left, top.head(1)});
		test(2,1,16, {bottom.head(2), left});
		test(3,2,8, {bottom, left.head(2)});

		/* now from the other sides */
		test(4,8,2, {left.tail(3), top, right, bottom.tail(1)});
		test(8,4,7, {left.head(2), bottom, right.head(1)});
		test(17,18,0, {top.tail(2), right, bottom});
		test(19,18,8, {top, left.tail(2)});
		test(7,11,0, {right.tail(3), top, left});
		test(11,7,17, {right.head(2), bottom, left, top.head(1)});
	}
}